查看原文
其他

Python之BeautifulSoup模块:处理HTML文档的利器

爬虫俱乐部 Stata and Python数据分析 2022-03-15

本文作者:张   邯

编辑作者:胡   婧

技术总编:刘鸿儒

有问题,不要怕!点击推文底部“阅读原文”下载爬虫俱乐部用户问题登记表并按要求填写后发送至邮箱statatraining@163.com,我们会及时为您解答哟~

爬虫俱乐部的github主站正式上线了!我们的网站地址是:https://stata-club.github.io,粉丝们可以通过该网站访问过去的推文哟~

好消息:爬虫俱乐部隆重推出数据定制及处理业务啦,您有任何网页数据获取及处理方面的难题,请发邮件至我们邮箱statatraining@163.com,届时会有俱乐部高级会员为您排忧解难!

使用爬虫爬取数据的时候,我们需要在已经获得的网页源代码文本中提取真正需要的信息。在以往的推文中,我们已经介绍过了正则表达式的使用方法,但是对于处理XML(Extensible Markup Language,可扩展标记语言)、HTML(HyperText Markup Language,超文本标记语言)这种结构化的文本数据,使用复杂的正则表达式实在是小题大做。更何况表达式写出来后,第二天自己都可能已经看不懂是怎么写出来的,代码的可读性极差。当需要做另外一个数据清洗工作时,又要重新分析源代码的特征、思考如何写表达式,这对于主攻实证分析、不擅长也无心于数据清洗的读者们是一个痛苦的也是无用且耗费时间的工作。

据了解,BeautifulSoup是一个第三方库,它提供若干函数,实现了处理导航搜索修改文档树的多种功能,不需要多少代码就可以写出一个完整、简单、易懂的数据清洗程序,是对HTML文档进行处理的极好方法库。

01

浅析HTML文档的结构

做过爬虫的人对网页源代码并不陌生,经常需要通过观察思考如何定位目标信息,从而实现数据清洗。不知大家是否深入地观察过整个HTML文档,标准的HTML文档都具有一个基本的整体结构,标记一般都是成对出现(部分标记除外例如:<br/>),下面展示一个简单的HTML文档。

整个文档分为头部(页面的标题、说明、序言等内容,不作为内容显示)和主体(网页中显示的实际内容),由不同的成对标签分隔成不同的元素,通常会出现一对标签中包含另一对标签的情况,此时出现了“级别”不同的标签。浏览器通过解析这些标签及其属性(如class、id、href)进行渲染,就可以将要实际显示的信息以丰富多彩的结构形式展现给用户。

通过上面的分析,我们可以得到一种处理HTML文档的思路:将不同标签及其属性和内容进行分级整理,即可得到一个文档“树”,像剥洋葱一样一层一层地剥开,直到最后一层“剥”成字符串等信息,这些信息通常都是最后显示在浏览器上的信息,也通常是我们感兴趣的信息。基于这个想法可以将上述HTML文档画成如下示意图。

如此整理后,我们就可以很方便、很直观地得到我们想要的目标信息了。BeautifulSoup库正是将HTML源码解析成如上的文档“树”形式,以便从该源码中快速定位目标信息,而我们的主要的工作就是如何定位到它们。

02

BeautifulSoup的使用方法入门

作为第三方库,BeautifulSoup在使用之前需要进行安装,一个简单的方式是在cmd中输入以下命令:

pip install BeautifulSoup4

每次在Python中使用时需要进行导入,命令如下,需要:

from bs4 import BeautifulSoup

接下来,我们将上述HTML文本文档输入并创建BeautifulSoup对象 soup ,其中soup为对象名,读者可以根据自己的命名习惯来自行命名;lxml为指定的解析引擎,同样是第三方库,需要安装,若读者没有安装请自行安装,若有其他习惯使用的解析引擎也可自行替换。命令如下:


html_text = '''    <html>    <head><title>我的第一个 HTML 页面</title></head>    <body>    <p class="haha" id = "12300" href = "http://wuhanstring.com">body 元素的内容会显示在浏览器中。</p>    <p class="haha1">title 元素的内容会显示在浏览器的标题栏中。</p>           </body>    </html> '''
soup = BeautifulSoup(html_text,"lxml")


此处提示两点,其一,BeautifulSoup中的B和S需大写,小写会报错;其二,BeautifulSoup自动将输入文档转换为Unicode编码,输出文档转换为utf-8编码,所以当使用requests.get获得HTML文档后用它进行处理时,不需要考虑编码方式

到此,所有的准备工作就完成了。接下来我们简单介绍几个命令,向大家展示一下BeautifulSoup的魅力!

获取所需节点

1. 通过 标签名(tag_name) 和标签属性(attrs)进行定位

① .tag_name

通过指定名称,可以直接获得该标签的所有内容。使用时可以写多级,但需注意只能获得第一个标签。比如soup.body.p,它表明要获取soup对象下,第一个'body'标签下的第一个'p'标签(也就是说,第二个'p'标签的内容不能通过这种方式获取)。例如键入示例代码,我们可以得到如下结果:

soup.body.p

② .find()

这条命令可以增加筛选条件,比如同样是'body'中的'p'标签,但是我们所需要的是class属性值为'haha1'的那一个,此时可以键入示例代码,获得如下结果:

soup.body.find('p',class_='haha1')

需要注意一下,class是Python中的保留字,为了避免语法错误,使用class属性时要用class_

简单介绍一下这条命令的几个参数:

Tag_name指标签的名称,常见的名称有'html'、'body'、'p'、'a'等。

attrs指标签的属性,如class、id、href、src,具体使用过程中,可以根据HTML文档进行分析,使用可以标记特征的属性值。示例中使用了class属性。若要定位“body 元素的内容会显示在浏览器中。”这个字符串,则可以使用id='12300'或href="http://wuhanstring.com" 。

此处并没有将所有的参数介绍全,仅介绍了入门使用可能用到的过滤器条件,通过这些条件,我们可以组成一个过滤器,过滤出我们想要的内容。

③ .find_all()

这条命令与上条命令重要的区别是得到一个标签还是得到所有标签,前者返回一个结果,若找不到则返回None,后者返回一个列表若找不到则返回空列表。参数完全相同,不再赘述,这里介绍一下配合循环的具体用法:

for unit in soup.find_all('p'):    print(unit)

此时find_all('p')会将所有的'p'标签都找到,放入一个列表当中,我们可以通过for循环调取每个元素进行输出。

2. 通过节点间关系和解析顺序进行定位

我们重新观察文档“树”,“树”上的节点之间的基本关系有三种:子节点、父节点和兄弟节点。父子关系描述上下级节点,如<html>标签和<body>标签的关系;兄弟关系描述同级节点,如<body>标签和<head>标签之间的关系。同一个节点相对不同的其他节点来说,会有多重身份,比如<body>标签,它相对于<html>标签是子节点,相对于<head>标签是兄弟节点。这为我们提供了另一种定位思路:当我们所需的信息所在的标签的标签名称出现在整个文档的多个地方,并且没有class属性、id属性、src属性或href属性的“个性特征”,此时可以借助它有“个性”的“亲戚”来间接定位。 

① .children

一个节点可以有多个子节点,获取时会生成一个迭代器。例如,获取'html'下的子节点:

for child in soup.html.children:    print(child)

子节点放在一个迭代器中,我们用循环进行输出,此时得到所有的子节点:

② .parent

一个节点只有一个父节点,例如,获取'body'的父节点:

soup.body.parent

由于'body'标签的上一级是'html'标签,所以我们得到了整个'html'标签的内容。

 .next_element&.previous_element

.next_element(.previous_element)会向前(后)访问一个解析对象(标签或字符串),这需要用到HTML文档解析的相关知识,可以简单地理解为,有子节点时访问子节点,没有子节点时访问下一个兄弟节点,没有兄弟节点时访问父节点的兄弟节点。举例说明:

soup.head.next_element

soup.head.next_element.next_element

.previous_element与之方向相反,读者可自己尝试一下。

获取节点内容

经过前述的定位后,我们可以得到目标信息的标签位置,现在就可以用“.string”这条命令进行数据提取了,此时会调用这个标签的子节点中的string(字符串)对象。示例命令如下:

print(soup.find('p').string)

此时输出soup.find('p')中的字符串:

值得一提的是,当标签中只有一个string对象时,则输出此string对象的内容,但当有多个string对象的时候,“.string”方法无法确定调用哪个子节点的内容,则会返回none。此时我们需要用“.strings”来获取这多个字符串的内容,并使用循环进行输出。

03

实战演练

接下来我们以爬虫俱乐部的主页为例,用BeautifulSoup来做一个简单的数据清洗工作:获得爬虫俱乐部网页首页的业务介绍、链接及简介。

首先,我们观察源代码:

显然,我们不能通过简单的标签名称来定位所需的信息,因为'div'标签实在是太多了,并且很难通过他的父节点进行定位。所以我们需要一些特定的属性来找到标签位置或与之相近的标签,通过各种关系定位到目标标签。

通常'class'作为属性,具有一定程度上的标识能力。我们观察到,所有的标题字符串所在的标签,'class'属性值为'service-name',通过页面查找可以知道,整篇源代码仅仅在这里才有这个属性,这无疑成为最好的定位方式。接下来,我们发现链接所在的标签'a'是'div'这个标签的子标签,同时也是下一个要解析的对象,所以我们可以用.children或.element_next的方法来获得,如果使用.children时需要注意,这个方法返回的是一个列表。至于业务简介,我们也可以如法炮制,定位到它所在的标签,然后输出字符串内容。当然,定位方式并不唯一,这里仅给出此种方法的代码供读者参考:

#获取网页源代码,生成BeautifulSoup对象
import requests content = requests.get("http://wuhanstring.com/") text = content.text
from bs4 import BeautifulSoup r = BeautifulSoup(text,"lxml")
#列出业务种类、链接及简介
for unit in r.find_all('div',class_='service-name'):    print('业务名称:',unit.string)    print('业务链接: http://www.wuhanstring.com',unit.next_element['href'])    tag_text=unit.next_element.next_element.next_element.next_element    print('业务简介:',tag_text.string)    print('------------------------------')

可以得到如下结果:

 我们也可以发现,所有的简介所在标签的'class'属性为'service-text',这也可以成为简介的定位方式,读者可以自己尝试一下。 

以上,即为BeautifulSoup模块的入门介绍,不难体会到,这个模块的确是数据清洗时的利器,尤其是当HTML文档结构清晰、内容简洁时,它的优势尤其明显。在后续的推文中,我们将继续介绍一些BeautifulSoup其他有用、有趣的函数和使用方法。

不知今天这碗“美丽鸡汤”,大家是否觉得可口?

对爬虫俱乐部的推文累计打赏超过1000元我们即可给您开具发票,发票类别为“咨询费”,目前第五批发票已经寄到各位读者的手中。用心做事,只为做您更贴心的小爬虫!

往期推文推荐:


关于我们

微信公众号“爬虫俱乐部”分享实用的stata命令,欢迎转载、打赏。爬虫俱乐部是由李春涛教授领导下的研究生及本科生组成的大数据分析和数据挖掘团队。


此外,欢迎大家踊跃投稿,介绍一些关于stata的数据处理和分析技巧。

投稿邮箱:statatraining@163.com

投稿要求:
1)必须原创,禁止抄袭;
2)必须准确,详细,有例子,有截图;
注意事项:
1)所有投稿都会经过本公众号运营团队成员的审核,审核通过才可录用,一经录用,会在该推文里为作者署名,并有赏金分成。
2)邮件请注明投稿,邮件名称为“投稿+推文名称”。

3)应广大读者要求,现开通有偿问答服务,如果大家遇到关于stata分析数据的问题,可以在公众号中提出,只需支付少量赏金,我们会在后期的推文里给予解答。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存